package de.ghost23.common.process {
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;

	/**
	 * A Process is a sequence of ICommands.
	 * These commands are executed in an asynchronous manner. Means, their execute method is called and
	 * Process listens for a ProcessEvent.COMPLETED event before proceeding with the next ICommand in the
	 * queue.
	 *
	 * <p>A Process can be defined in two different ways. Either
	 * <ul>
	 * <li>by extending this Process class and defining the queue in the constructor, or</li>
	 * <li>by using Process directly and defining the queue from outside.</li>
	 * </ul>
	 * </p>
	 * <p>A Process also offers a data Dictionary, which serves as a container for all kinds
	 * of objects and variables. The Process passes the data Dictionary to each running
	 * ICommand. This way, the commands share a container for values and other stuff. a concrete implementation
	 * of a process for example could add some initial values, that some of the commands might need for operation.</p>
	 *
	 * <p>The Process needs explicitly to be started with start(). During execution, the Process can be stopped using
	 * stop(). For resetting the queue completely (sets it to null actually), use clear().</p>
	 * @example
	 * An example for the case 'Extending Process' follows (import statements have been left out):
	<listing version="3.0">class de.ghost23.processtest.SampleProcess extends Process {

	public function SampleProcess() {

		super();

		queue = new &lt;Class&gt; [
			SampleCommandOne,
			SampleCommandTwo,
			SampleCommandThree
		];
	}
}</listing>
	 * <p>Note, that the queue is made up of references to the classes directly, not class instances.
	 * Process uses lazy instantiation, so an instance of the respective ICommand is created directly
	 * before executing the command.</p>
	 *
	 * <p>Here comes an example for the case 'Using Process directly' (import statements have been left out):</p>
	 * @example
	<listing version="3.0">var sampleProcess:Process = new Process();

sampleProcess.pushCommandStep(SampleCommandOne);
sampleProcess.pushCommandStep(SampleCommandTwo);
sampleProcess.pushCommandStep(SampleCommandThree);

sampleProcess.addEventListener(ProcessEvent.COMPLETED, this);
sampleProcess.addEventListener(ProcessEvent.FAILED, this);
sampleProcess.start();</listing>
	 * @author Sven Busse
	 * @version 1.0
	 * @see de.ghost23.common.process.ICommand
	 */
	public class Process extends EventDispatcher {
		
		/**
		 * Queue holds class references (! not instances) of ICommand. You only use queue,
		 * when subclassing Process to build a concrete one. See the examples for more information.
		 */
		protected var queue:Vector.<Class>;
		
		/**
		 * The currently pending ICommand index
		 */
		private var currentCmdIndex:Number;
		
		/**
		 * The currently pending ICommand
		 */
		private var currentCmd:ICommand;
		
		/**
		 * Holder for various param objects or primitives, that are needed by
		 * either the process or the commands. This Dictionary is passed to every
		 * command, once it is executed. So, this way, command can exchange data,
		 * or data can be passed to and from them from outside of the process.
		 */
		private var _data:Dictionary = new Dictionary();
		
		/**
		 * Starts the process. Do not forget to add Listeners to the ProcessEvent.COMPLETED and
		 * ProcessEvent.FAILED events before calling start, in case your Process is synchronized.
		 */
		public function start():void {
			currentCmdIndex = -1;
			reset();
			doNext(null);
		}
		
		/**
		 * Stops this process, calls cancel at the currently running Command, BUT does not
		 * listen to the FAILED event of that command anymore. Thus, this process
		 * will not dispatch a ProcessEvent.FAILED.
		 */
		public function stop():void {
			reset();
		}
		
		/**
		 * Clear does also stop the Process in case it was running (see stop for details). In addition,
		 * the qeue and the ParamContainer is set to null. This means, you cannot start this Process
		 * again, unless you fill the queue again by using pushCommandStep.
		 */
		public function clear():void {
			reset();
			queue = null;
			_data = null;
		}
		
		/**
		 * Pushes a new ICommand into the queue. Each new ICommand is added at the end of
		 * the queue.
		 * @param	newCommand The new ICommand, that should be added at the end of the queue.
		 * Note, this should be a reference to the class directly, not a class instance!
		 */
		public function pushCommand(newCommand:Class):void {
			
			if (queue == null) queue = new Vector.<Class>();
			queue.push(newCommand);
		}

		/**
		 * Holder for various param objects or primitives, that are needed by
		 * either the process or the commands. This Dictionary is passed to every
		 * command, once it is executed. So, this way, command can exchange data,
		 * or data can be passed to and from them from outside of the process.
		 */
		public function get data():Dictionary {
			return _data;
		}
		
		/**
		 * Removes the listener of the currently running ICommand, then cancels the ICommand
		 * and sets the references to null.
		 */
		private function reset():void {
			
			if(currentCmd == null) return;
			currentCmd.removeEventListener(ProcessEvent.COMPLETED, doNext);
			currentCmd.removeEventListener(ProcessEvent.FAILED, onCommandFailed);
			currentCmd.removeEventListener(ProcessEvent.PROGRESS, onCommandProgress);
			currentCmd.cancel();
			currentCmd = null;
		}
		
		/**
		 * Processes the next ICommand. Does so by instantiating the class reference and then
		 * starting the command. If no ICommand is left, end is called.
		 */
		private function doNext(evt:ProcessEvent):void {
			
			if(currentCmdIndex > 0) dispatchEvent(new ProcessEvent(ProcessEvent.COMMAND, null, queue[currentCmdIndex]));

			if(currentCmdIndex+1 < queue.length) {
				currentCmdIndex++;
				if(currentCmd != null) {
					reset();
				}
				
				currentCmd = new queue[currentCmdIndex]();
				currentCmd.addEventListener(ProcessEvent.COMPLETED, doNext, false, 0, true);
				currentCmd.addEventListener(ProcessEvent.FAILED, onCommandFailed, false, 0, true);
				currentCmd.addEventListener(ProcessEvent.PROGRESS, onCommandProgress, false, 0, true);
				currentCmd.execute(_data);
				updateProgress(0);

			}else {
				updateProgress(1);
				end("200");
			}
		}

		/**
		 * If an ICommand dispatches a ProcessEvent.PROGRESS, the overall progress is recalculated
		 * and dispatched to the consumer.
		 * @param event The ProcessEvent.PROGRESS, contains the progress number
		 */
		private function onCommandProgress(event:ProcessEvent):void {
			updateProgress(event.progress);
		}

		/**
		 * Updates the progress of this process and dispatches it to the consumer.
		 * @param currentCommandProgress A number between 0 and 1, indicating the progress of the process.
		 */
		private function updateProgress(currentCommandProgress:Number):void {

			var overallProgress:Number = (currentCmdIndex + currentCommandProgress) / queue.length;
			dispatchEvent(new ProcessEvent(ProcessEvent.PROGRESS, null, null, overallProgress));
		}
		
		/**
		 * If an ICommand dispatches ProcessEvent.FAILED, then reset is called and this Process
		 * dispatches ProcessEvent.FAILED, passing through a reference to the ICommand, that
		 * failed.
		 * @param evt EventHandler argument, the event object
		 */
		private function onCommandFailed(evt:ProcessEvent):void {
			reset();
			dispatchEvent(new ProcessEvent(ProcessEvent.FAILED, null, queue[currentCmdIndex]));
		}
		
		/**
		 * Called, when the Process has finished. Resets the Process and dispatches ProcessEvent.COMPLETED.
		 */
		private function end(endResultCode:String):void {
			reset();
			dispatchEvent(new ProcessEvent(ProcessEvent.COMPLETED, null, queue[currentCmdIndex]));
		}
	}
}